Unity アセットで外部エディタと連携する裏側


概要

夏だ!! アドベントカレンダーだ!! ということで

Unity アセット真夏のアドベントカレンダー 2014 Summer!

http://unityassetjp.doorkeeper.jp/events/12843


先日はyandoさんによる「Detonatorと爆発の未来」でした。

本日は8/4 の記者、sassembla がお送りします。



本題

Unityでコード編集するときに使ってる、自作のAssetがありまして。

今回はその裏側で、こんなテクニックを使ってるよー、みたいなのを紹介する。


肝心のAssetはこんなのです。 Unity と Sublime Text を連携するやつ。

スクリーンショット 2014-08-03 14.39.03.png


AssetStoreに上がってるので良かったらどうぞ!

http://u3d.as/content/sassembla/sublime-socket-asset/4SP

コンパイルエラーをエディタ上に表示したり、

ログをコード上の発生箇所に時系列でマッピングしたり、

プロジェクト完全依存な補完出したりできる。


このAssetは Unity Editorと、連携用のUnityAsset(上記のやつ)と、外部エディタ、そして外部エディタ内で動くプラグインで実現されている。

主に次のような機能で、Unityと外部エディタの連携を実現している。


1.Unityと外部エディタを接続する

2.Unityにコード全体をコンパイルさせる

3.Unityに外部プロセスから情報を送り込む

4.Unityからコンパイル時の情報を引き出す

5.Unityからコード補完時の情報を引き出す

6.Unityから引き出した情報をもとに、エディタに働きかける

それぞれ作ってておもしろかったポイントがあるので、順に書いていく。



1.Unityと外部エディタを接続する

MonoDevelopと違って、Unityが提供しているアタッチ系の機能を使用していない。

WinとMacで違ってて大変うっとうしかったためだ。リバースエンジニアリングに近かったし。


というわけで、Unityの力を借りず、接続にはWebSocketを使っている。

図にするとこんな感じ。

スクリーンショット 2014-08-03 16.37.51.png


左 Unity + SublimeSocketAsset がwsクライアントで、

右 Sublime Text + プラグインがwsサーバになっている。



なんでUnity + アセット側がwsクライアントなの?


やんごとなき理由があった。


・Unityのエディタはコード変更の度に毎回全体をコンパイルする = ゲームコードもエディタもコンパイルされ直す -> サーバになれない

・wsだったら双方向同期/非同期で通信できる

・Mac/Winとかで統一的な方法が欲しい

・別サーバで動かしているUnityを使役したいみたいな欲がある(リモートコンパイラ)

みたいなニーズと理由があったため。



特にサーバになれない理由を、もうちょっとちゃんと紹介する。

根本的なところは、Unity Editor が持つコードコンパイルの構造にある。



Unity Editor の適当なコンパイル構造は次のような感じ。

スクリーンショット 2014-08-03 16.57.48.png

Management Layer (正式名称は知らない)

UnityでのC#コードとかのコンパイルほかを管理しているレイヤ。appとかexeとして実行されているコア。


Editor Script Layer 

/Editor 名称のフォルダ以下、エディタのパーツとしてコンパイルされるコードが居るレイヤ。


Game Script Layer 

ゲームのコードとかが居るレイヤ。


★この図はいろいろ端折ってあるので、詳しくはここをみるといいと思う。

http://docs.unity3d.com/Manual/ScriptCompileOrderFolders.html



Editorのレイヤには、/Editor フォルダ名で置いたコードをコンパイルする規約があるんだけど、

言い換えれば毎回Unityのコンパイルに巻き込まれる。

これはゲームスクリプトのレイヤのコンパイルが発生しただけでも、Editorから再度コンパイルされることを指している。


別に悪いことじゃないんだけど、外部との接続を行う際、ここがネックになる。

コンパイルが走る度に、通信をしていたプロセス自体がぶっ殺されて新たにコンパイルされて、コネクションが途切れるためだ。


だれも生き残らない。



で、これは仕組みでどうこうできるものでもなし、流儀だし、何かするのが悪手に思えたので、

できるだけ再接続しやすい、切断を考慮にいれた通信方式+形態は無いか? という観点から、

ぶっちぎれ易い方/にくい方を区別して設定できて、

再接続がスムーズにいくようにデザインされてて、

かつ規格として実装流通が多いもの、としてwsを採用した向きが強い。


Unityがエディタ部分のコードをコンパイルした後、自動的に Sublime Text が動かしているwsサーバへと接続される。


もしリデザインする機会があったら、wsの代わりにMQTTを使う。オーバースペックか、、

(Sublime Textのプラグイン側には実験中のリポジトリがある。



★ちなみにこうやってみるとEditor -> Gameと順次コンパイルしているように見えるが、

実際にはEditor レイヤのコード + Game レイヤのコードを纏めた上でコンパイルしているので、

Editorレイヤのコンパイルが終わったら、Game側のコードのコンパイル前にPrecompile処理を仕掛ける、みたいなのはできない。


強制的にPrecompileする処理は可能なんだけど、どっちにしてもコンパイル後に動き出す感じになる。

てことはそれ無限ループするんじゃね?という感じ。



2.Unityにコード全体をコンパイルさせる

接続が確立された Sublime Text エディタでの保存処理をトリガーにして、

Unityにコンパイルを実行させることは、実はめっちゃ難しかった。

というのも、公式な手段が無い。

Unity Editor には、Editorに表示させたメニューからコードを変更させたり、

特定のコードをコンパイルさせる手段も存在している、、のだけれど、


これらは Main Thread と呼ばれる、Unity Editor ががっちり抱えている Thread からしかコントロールできない。

で、Unity Editor 外部からそれら Main Thread に合流してコンパイルを実行する、みたいな処理は存在しない。


ので、とてもDirtyなのだけれど、

Unity.app/exe へとフォーカスを移すと、Unityはコンパイルを始める

という事象を利用している。

スクリーンショット 2014-08-03 17.46.25.png

まったくもってバカバカしい悪手なんだけど、これが一番案配がよかった。


このへんはさすがにプラットフォームごとに互換性が無かったので、同じインターフェースで動くようにコマンドラインツールを用意した。

Mac用

https://github.com/sassembla/SwitchApp

Win用

https://github.com/sassembla/WinSwitchApp


もうちょい正しくマシな手段ができないかなー、と思っている。



ちなみにこの手段を使う上でのMonoDevelopでのビルドとの差異はかなり存在していて、


・MonoDevelopは Unity Editor が生成した.slnファイルを使用することでコンパイルパスとかリソースをプロジェクトに組み込んでいて、

MonoDevelopのコンパイルは、Slnを通じてUnityのライブラリのパスとかを使って実行しているに過ぎないので、

Unityでコンパイルしたときとは異なる挙動になる。(まあ問題がある程とは思ってないけどもんにょりする)

・上記のフォーカス動かす方法だと、そもUnity Editorに直接コンパイルを命令しているので、

Unityそのもののコンパイルがそのまま実行される形になる。


この際、assetの読み込みなどに関連する情報も出力されるため、外部に対してより多い情報や処理を持ち出すことができる。

そのかわり、単純にコンパイルしてる訳ではないので、重い。


で、Unity 4.3? 以降だと、コードのhash値かなにかに差分がないとコンパイルされないようになったので、

コメント内のタイムスタンプのみを更新するファイル、とかを使って、強制的にコンパイルが発生するようにしている。

それ以前だと、保存した時刻をみてたみたいだ。


うーんまあこれも悪手。カットできるようにはしたけど。



3.Unityに外部プロセスから情報を送り込む

wsでの接続を介して、Sub Thread 扱いでSublime Text -> Unity Editor への入力を実行できる。


結果として Sub Thread でのUnity Editorのコード実行になるので、

APIの内容によっては、Main Thread からしか呼べないよ! 的なエラーが発生する。


そのため、Main Thread でしか動作できないスクリプトは必然的に全く使えない。



これが、Unity Editor のGUIに関わるコードなら納得できるんだけど、そうでないにも関わらず Main からしか呼べない、みたいなのが結構ある。

GetPath系とか。



また、Sub Thread で来た入力を、そのまま Main Thread へと合流する手段がない。

エディタで動くコードなので、Coroutineでなんとかする、みたいな合流手段も使えない。


下図のような感じ。

スクリーンショット 2014-08-03 20.57.02.png

このへんは割と設計全体に関わる感じになる。



Main Threadでしかできないことを Sub Thread で行うことはできないが、


Sub Thread でも使いたい、Main Thread Method Depend なパラメータがあったら、

Main Thread で実行される InitializeOnLoad 時に取得してstaticなパラメータに入れておくことで、

Main Thread でしか取得できないパラメータへのアクセスが可能っちゃあ可能になっている。



ただ、WebSocketでの入力を、Unityのコンパイル中に受けると、クリティカルなタイミングで Unity Editor 自体が死ぬ。


何かエラーを出力してくれるとうれしいんだけど、Unity Editor と被コンパイル対象との間に微妙な齟齬があるようで、

本当にクリティカルなタイミングだと、何のエラーも吐いてくれない。

このへんはそのうち Sub から Main への合流手段が提供されるかもなーと思っている。


ちなみになぜ Main Thread でwsのpushを受けないか、というと、

Mainで受けようとすると Unity Editor それ自体が完全に停止するから。 


Main Thread の流れを阻害してはいけない。ナムアミダブツ。


Sub Thread バンザーイ!!!!



4.Unityからコンパイル時の情報を引き出す

Unity Editor にコンパイル命令を入力できれば、Editorのログファイルにコンパイル状況が書き込まれる。

ログファイルはMacとWinでパスが異なるが、複数プロジェクトがあっても基本一つのファイルをみている。


Mac

Users/Library/Logs/Unity/Editor.log

Windows

Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + ¥Local¥Unity¥Editor¥Editor.log


このファイルを、Editor側で動くtailのようなTimerロジックから読み込み、

読み込んだ結果をwsでエディタに送り込んで、コンパイルエラーなどをエディタに表示させている。


この方法の優れたところは、コンパイル中、更にはゲーム実行時のログをストリーミングしてコード上に反映させることができるため、

リアルタイムにコード上のどの値がどう変わったか、をその発生箇所ごとに記録できること。

動画では、00:45あたりから、Debug.Log(string) 関数に対して、L: という文字列を頭に付与することで、

「該当ログの実行記録をこの行に時系列順に表示する」ということを行っている。

例えばゲーム作りの途中で、挙動が複雑なところがあって、そのコードや通過しているかどうかの判定がしたい場合、

どの順番でどこを通ったか、などが目で見て判定可能になる。



5.Unityからコード補完時の情報を引き出す

コード補完は、Sublime Text から Unity Editor、 Unity Editor から Sublime Text へと、複数の非同期/同期通信を組み合わせて実現している。

具体的なシーケンスの例をあげると、


Something.(ドット) とかが入力された

Somethingが型なのかインスタンスなのかプロパティなのかフィールドなのか解析

・var だったら実際にはどんな型なのか解析

・使用されてるusingとかから補完に現れるべき範囲を限定

・補完候補を出力する


といった事をしている。


図にするとこんな感じ。

スクリーンショット 2014-08-03 23.59.54.png

Sublime Text からのInputに対して、キャッシュがある場合は高速なoutputを返し、

キャッシュがない場合は Unity Editor 側で解析と補完情報を出している。


編集中のコードを相手にするので、ほとんどキャッシュがあるはずが無い感じになる。

さらに、コードが中途半端でも解析できないといけない。


この解析まわりは、NRefactoryをUnity用に大改造することで対応している。

UnityのC♯のバージョンにあわせて改変しまくったものを使用している。


送り込まれてきたコードは、実際にはまだ未保存のはずで、 Sublime Text のバッファ上にしか無くて、

key input のたびにドカッとコード全体を送りつけている。

しかしそこはWebSocket、十分に高速。



で、コードの解析には時間がかかるので、解析は非同期な Thread を作成し、解析が終わったら、補完情報を取得、Sublime Text にPushする。

その補完情報の収集のために、Unityのapp + プロジェクト構造の中から、下記DLLを読み込んでいる。


・/Applications/Unity/Unity.app/Contents/Frameworks/Managed/UnityEngine.dll ・/Applications/Unity/Unity.app/Contents/Frameworks/Mono/lib/mono/unity/mscorlib.dll

Unityのライブラリ一式と、C♯用のライブラリ。

・Project/Library/ScriptAssemblies/Assembly-CSharp.dll

プロジェクトのコンパイル結果が入るDLL。

Editor 部分のコードのほか、コンパイルに巻き込まれたすべてのdllが含まれている。

これらすべてのdllの中から、実際に編集中のコードでusingされている内容を絞り込み、

型を特定して、補完情報を送付している。


ちなみに件数が膨大になった場合、ある程度の流量になるように補完情報を分割して送付する、みたいなことをしている。

これによって Sublime Text 側のUI的なロックは無いようになっている。



6.Unityから引き出した情報をもとに、エディタに働きかける

Sublime Text 側に、入力に応じてエディタの動作を設計できるパイプライン的な設定がなされている。

ぜんぜんUnity関係ないけど、方法の一つとして紹介だけ。


Unity Editor から特定のフォーマットの文字列を送り込むことで、フローから Sublime Text のAPIを実行できる。


内容にはSushiJSONという「JSONを書いたらフローが決定される」自作のDSLを使っている。

SushiJSON

https://github.com/sassembla/SushiJSON



たとえば下記のようなパイプライン処理を、Unity Editor 側からセットして実現している。

スクリーンショット 2014-08-04 1.08.03.png


内容によってfilteringして、入力された内容が正規表現にマッチしたら、あらかじめ

記述していたフローに沿ってAPIを発動させる。

さらにその結果出てきたインターフェースに対してアクションをして、何が発生するか、みたいなのも指定できる。

例えばエラーをエディタ上に表示したい場合、

エラーっぽいログが流れてくる

→エラー表示をコード上に出す

→クリックされたらエラーを表示するイベントをセットする


→クリックされたのでエラー内容を表示

→表示された内容がクリックされたら何かするイベントをセットする


→クリックされたら、、、


みたいに、エディタのAPIフローを予約できる。

filteringも多重化できるので、やり放題。

こういうのがいろんなエディタにフロー型の記述方法で入らないかなー。



Unity Editor と Mono Develop への反逆を通じてわかったこと、得たもの

エディタには我慢しない方がいいなーということなど。


派生物がかなり多くなったので、やってみてよかった。

たとえば、

外部プロセス使ってゲームのプレイデータの蓄積とか解析とかを常日頃かけておけると、面白い.


例えば開発中のゲームに対して「どこが遅いかをクラス単位で計測するIL」を仕込んだりとか、

デバッガーさんとかにプレイしてもらって「どこのステージのどの手順で敵が100 -> 10 -> 0になるまでにかかった時間をグラフにする」とか、

ゲーム作りの中で計測すべきあらゆる物事を、事前に計測しておくことができるようになったりする。

まあ通信機構入れて逐一解析DBにぶち込んで解析すれば一緒だと思うけど。


こまかい粒度のことは、コードを書いてるそのときに知りたい。



明日は、Dvorakはしもと さんです!